﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Data;

/*
	Class to hold lookup data
	Exposes the Xml docs, defines the tags to be used and 
	creates nodes etc.
	Kept separate from main config because this may grow large if
	the auto populate from FK feature is added.
	
	Use singleton pattern and have a static instance object 

*/

namespace DbView.Resources
{
	public class LookupInfo : XmlDocument
	{
		// elements and atts found in the lookup file 
		// Presently: DbViewLookups.xml
		public const String LUELM_DOC = "Lookups";
		public const String LUELM_DRIVER = "Driver";
		public const String LUELM_CONNECTION = "Conn";
		public const String LUELM_TABLE = "Table";
		public const String LUELM_LOOKUP = "Lookup";
		
		public const String LUATT_FIELD = "field";
		public const String LUATT_TABLE = "table";
		public const String LUATT_NAME = "name";
		public const String LUATT_TYPE = "type";
		public const String LUATT_REPORTFIELDS = "report";
		public const String LUATT_LOOKUPTABLE = "pktable";
		public const String LUATT_LOOKUPFIELD = "pkfield";

		// class to hold the items that identify the lookup data
		// table.field of the FK table plus the driver and connection name
		public class Key
		{
			public string Driver {get; set;}
			public string Connection {get; set;}
			public string Table {get; set;}
			public string Field {get; set;}
			
			public Key(string driver, string connection, string table, string field)
			{
				this.Driver = driver;
				this.Connection = connection;
				this.Table = table;
				this.Field = field;
			}
			public Key() : this("","","","") {}
			public override string ToString()
			{
				return string.Format("{0}.{1} {2}.{3}", this.Driver, this.Connection, this.Table, this.Field);
			}
		}
		
		// class to hold the data for the lookup report
		// the Pk table.field and the fields selected for display from the PkTable
		public class Data
		{
			private List<string> reportFields = new List<string>();
			public string PkTable {get; set;}
			public string PkField {get; set;}
			public string FieldType {get; set;}
			public string[] ReportFields {get { return this.reportFields.ToArray(); } }
			public void AddReportField(string reportField)
			{
				this.reportFields.Add(reportField); // prevent dups?
			}

			public override string ToString()
			{
				return string.Format("{0}.{1}[{2}] ({3} reported)", this.PkTable, this.PkField, this.FieldType, this.reportFields.Count);
			}
		}

		// possibly obsolete
		//public static LookupInfo.Key MakeKey(string driverName, string connectionName, string tableField)
		//{
		//    // a.b.c  or d.e for schema/no-schema c and e are fields 
		//    int ipos = tableField.LastIndexOf('.');
		//    if (ipos <= 0)
		//        throw new Exception(string.Format("MakeKey invoked with invalid table field: [{0}]\nExpected table.field or schema.table.field", tableField));
		//    string table = tableField.Substring(0, ipos);
		//    string field = tableField.Substring(ipos + 1);

		//    return new DbView.Resources.LookupInfo.Key(driverName, connectionName, table, field);
		//}
		
		// Singleton instance to keep all the lookup data handy
		static LookupInfo theConfig = null;
	
		public static LookupInfo Instance
		{
			get 
			{
				if (theConfig == null)
					theConfig = new LookupInfo();
				return theConfig;
			}
		}
		
		LookupInfo()
		{
		}
		
		// Since load and save seem to be asymmetrical (load once at start, save many times)
		// this will expose the file used for the load.
		public String FileName { get; set; }
		
		// careful with name Load() is used by the base class
		public void LoadConfig(String fileName)
		{
			try 
			{
				FileName = fileName; // store the name for access by others
				this.Load(fileName);
			}
			catch (System.IO.FileNotFoundException)
			{
				// Add root element
				XmlNode node = this.CreateElement(LUELM_DOC);
				this.AppendChild(node);
			}
		}
	
		// careful with name Save() is used by the base class
		// original required file name. Still exposed in case copying/backup required
		// this used to copy the config file to the portable application area.
		public void SaveConfig(String fileName)
		{
			this.Save(fileName);
		}

		// New/Prefered: use filename given when loading the config.
		public void SaveConfig()
		{
			this.Save(FileName);
		}
		
		// out of date now only re-hydrate if necessary
		//public void Test()
		//{
		//    // dummy data
		//    LookupInfo.Data d = new Data();
		//    d.PkTable = "PKT1";
		//    d.PkField = "PKFLD1";
		//    d.AddReportField("A");

		//    // add some
		//    LookupInfo.Key key = new Key("Drv1", "Conn1", "Tbl1", "F01");
		//    this.AddLookup(key, d);
			
		//    key.Driver = "Drv2";
		//    d.PkTable = "PKT2";
		//    d.AddReportField("B");
		//    this.AddLookup(key, d);
		//    key.Driver = "Drv1";

		//    key.Table = "Tbl2"; key.Field = "F21";
		//    d.AddReportField("C");
		//    this.AddLookup(key, d);

		//    key.Table = "Tbl1"; key.Field = "F2";
		//    this.AddLookup(key, d);

		//    key.Connection = "Conn2"; key.Table = "Tbl200"; key.Field = "F201";
		//    this.AddLookup(key, d);

		//    // write away 
		//    this.SaveConfig("c:\\temp\\huh.txt");

		//    // now we lookup stuff

		//    LookupInfo.Data f = this.FindData(key);
		//    this.CheckData(f);

		//    this.RemoveLookup(key);
		//    f = this.FindData(key);
		//    if (f != null) throw new ExecutionEngineException("Remove test fail");

		//    key = new Key("Drv1", "Conn1", "Tbl1", "F01");
		//    LookupInfo.Key [] keys = this.FindKeysForConnection(key);

		//    int i = 10;

		//    keys = this.FindKeysForTable(key);
		//    i = 10;
		//}
		// part of internal test code
		//private void CheckData(LookupInfo.Data f)
		//{
		//    if (f.PkTable.Length == 0)  throw new ExecutionEngineException("Lookup test fail (no Table)");
		//    if (f.PkField.Length == 0)  throw new ExecutionEngineException("Lookup test fail (no Field)");
		//    if (f.ReportFields.Length == 0) throw new ExecutionEngineException("Lookup test fail (no report)");
		//    if (f.ReportFields[0].Length == 0) throw new ExecutionEngineException("Lookup test fail (report field blank)");
		//}

		// Adds or updates the lookup data for the given key
		public void AddLookup(LookupInfo.Key key, LookupInfo.Data data)
		{
			XmlNode rootNode = GetRootNode();
			XmlNode driver = this.FindDriver(rootNode, key, true); // find true = create if necessary
			XmlNode connection = this.FindConnection(driver, key, true); // find true = create if necessary
			XmlNode rec = this.FindLookup(connection, key, true); // find true = create if necessary
			
			AddDataToNode(rec, data);
		}


		// Return a list of fields which when combined with the key will define the lookup
		// possibly extendible to include other info, but not yet
		public LookupInfo.Data FindData(LookupInfo.Key key)
		{
			// if none set don't look
			if (this.DocumentElement == null) return null;

			XmlNode rec = Find(this.DocumentElement, key);
			if (rec == null)
			{
				return null;
			}
			// search not for field
			return DataFromLookupNode(rec);
		}

		public LookupInfo.Key[] FindKeysForConnection(LookupInfo.Key partialKey)
		{
			// partial key needs a driver and connection
			if (string.IsNullOrEmpty(partialKey.Driver) ||
				string.IsNullOrEmpty(partialKey.Connection))
				throw new Exception(string.Format("LookupInfo2.FindKeysForConnection called with missing key info: {0}", partialKey.ToString()));

			// driver
			string xpath = String.Format("/{0}/{1}[@{2}='{3}']/{4}[@{5}='{6}']/{7}", LUELM_DOC, LookupInfo.LUELM_DRIVER, LookupInfo.LUATT_NAME, partialKey.Driver,
																							LookupInfo.LUELM_CONNECTION, LookupInfo.LUATT_NAME, partialKey.Connection,
																							LookupInfo.LUELM_LOOKUP);

			return FindKeyList(partialKey, xpath);
		}

		public bool RemoveLookup(LookupInfo.Key key)
		{
			XmlNode rec = Find(this.DocumentElement, key);
			if (rec == null)
			{
				return false;
			} 
			this.RemoveRecord(rec);
			return true;
		}

		// Dereference a node into a Lookup Data object
		private LookupInfo.Data DataFromLookupNode(XmlNode rec)
		{
			LookupInfo.Data lookupData = new LookupInfo.Data();
			lookupData.PkTable = this.AttributeValue(rec, LookupInfo.LUATT_LOOKUPTABLE, "");
			lookupData.PkField = this.AttributeValue(rec, LookupInfo.LUATT_LOOKUPFIELD, "");
			lookupData.FieldType = this.AttributeValue(rec, LookupInfo.LUATT_TYPE, "I"); // default int if missing
			string reportFieldMemo = this.AttributeValue(rec, LookupInfo.LUATT_REPORTFIELDS,"");
			this.UnpackReportFieldMemoString(lookupData, reportFieldMemo);
			return lookupData;
		}

		private XmlNode GetRootNode()
		{
			XmlNode rootNode = this.DocumentElement;
			// Bootstrap from empty
			if (rootNode == null)
			{
				rootNode = this.CreateElement(LookupInfo.LUELM_DOC);
				this.AppendChild(rootNode);
			}
			return rootNode;
		}

		private void AddDataToNode(XmlNode rec, Data data)
		{
			this.AddAttribute(rec, LookupInfo.LUATT_LOOKUPTABLE, data.PkTable);
			this.AddAttribute(rec, LookupInfo.LUATT_LOOKUPFIELD, data.PkField);
			this.AddAttribute(rec, LookupInfo.LUATT_TYPE, data.FieldType);
			string reportFieldMemo = this.CreateReportFieldMemoString(data);
			this.AddAttribute(rec, LookupInfo.LUATT_REPORTFIELDS, reportFieldMemo);
		}
		
		////////////////////////////////////////////////////////////////////////
		// search through the document for elements at each level
		private XmlNode FindDriver(XmlNode parent, LookupInfo.Key key, bool createIfMissing)
		{
			string xpath = string.Format("{0}[@{1}='{2}']", LookupInfo.LUELM_DRIVER, LookupInfo.LUATT_NAME, key.Driver);
			XmlNode driverNode = parent.SelectSingleNode(xpath);
			if (driverNode == null && createIfMissing)
				driverNode = CreateNewChildWithAtt(parent, LookupInfo.LUELM_DRIVER, LookupInfo.LUATT_NAME, key.Driver);
			return driverNode;
		}

		private XmlNode FindConnection(XmlNode parent, LookupInfo.Key key, bool createIfMissing)
		{
			string xpath = string.Format("{0}[@{1}='{2}']", LookupInfo.LUELM_CONNECTION, LookupInfo.LUATT_NAME, key.Connection);
			XmlNode connectionNode = parent.SelectSingleNode(xpath);
			if (connectionNode == null && createIfMissing)
				connectionNode = CreateNewChildWithAtt(parent, LookupInfo.LUELM_CONNECTION, LookupInfo.LUATT_NAME, key.Connection);
			return connectionNode;
		}

		private XmlNode FindLookup(XmlNode parent, LookupInfo.Key key, bool createIfMissing)
		{
			string xpath = string.Format("{0}[@{1}='{2}' and @{3}='{4}']", LookupInfo.LUELM_LOOKUP, LookupInfo.LUATT_TABLE, key.Table, LookupInfo.LUATT_FIELD, key.Field);
			XmlNode lookupNode = parent.SelectSingleNode(xpath);
			if (lookupNode == null && createIfMissing)
			{
				lookupNode = parent.OwnerDocument.CreateElement(LookupInfo.LUELM_LOOKUP);
				this.AddAttribute(lookupNode, LookupInfo.LUATT_TABLE, key.Table);
				this.AddAttribute(lookupNode, LookupInfo.LUATT_FIELD, key.Field);
				// find position for it. Search for last field for the given table
				xpath = string.Format("({0}[@{1}='{2}'])[last()]", LookupInfo.LUELM_LOOKUP, LookupInfo.LUATT_TABLE, key.Table);
				XmlNode insertAfterNode = parent.SelectSingleNode(xpath);
				if (insertAfterNode == null)
					parent.AppendChild(lookupNode);
				else
					parent.InsertAfter(lookupNode, insertAfterNode);
			}
			return lookupNode;
		}

		///////////////////////////////////////////////////////////////
		// TODO: memo string needs to allow for | in a field?
		// a. find an unused token in string %1% %2% etc.
		// b. replace | in fields with token.
		// c. set token used in an att
		// d. create class to wrap this.
		
		// To save space in the file report fields are stored as an attribute value 
		// seperated by the pipe delimiter.
		private string CreateReportFieldMemoString(LookupInfo.Data data)
		{
			return string.Join("|", data.ReportFields);
		}

		private void UnpackReportFieldMemoString(LookupInfo.Data data, string reportFieldMemo)
		{
			string[] fields = reportFieldMemo.Split('|');
			foreach (string field in fields)
				data.AddReportField(field);
		}

		// Create and element with an att that identifies it. 
		private XmlNode CreateNewChildWithAtt(XmlNode parent, String childElementName, string attName, string attValue)
		{
			XmlNode node = parent.OwnerDocument.CreateElement(childElementName);
			this.AddAttribute(node, attName, attValue);
			parent.AppendChild(node);
			return node;
		}

		/////////////////////////////////////////////////////////////////////////////////////////
		// Searching.

		// find the lookup data given key data
		private XmlNode Find(XmlNode rootNode, LookupInfo.Key key)
		{
			string driverSearch = string.Format("{0}[@{1}='{2}']", LookupInfo.LUELM_DRIVER, LookupInfo.LUATT_NAME, key.Driver);
			string connectionSearch = string.Format("{0}[@{1}='{2}']", LookupInfo.LUELM_CONNECTION, LookupInfo.LUATT_NAME, key.Connection);
			string lookupSearch = string.Format("{0}[@{1}='{2}' and @{3}='{4}']", LookupInfo.LUELM_LOOKUP,  LookupInfo.LUATT_TABLE, key.Table, LookupInfo.LUATT_FIELD, key.Field);
			string xpath = string.Format("{0}/{1}/{2}", driverSearch, connectionSearch, lookupSearch);
			XmlNode node = rootNode.SelectSingleNode(xpath);
			return node;

			//string xpath = string.Format("{0}/{1}/{2}[@{3}='{4}' and @{5}='{6}']", LookupInfo.LUELM_DRIVER, LookupInfo.LUELM_CONNECTION, LookupInfo.LUELM_LOOKUP, 
			//                                                                LookupInfo.LUATT_TABLE, key.Table, LookupInfo.LUATT_FIELD, key.Field);

			//XmlNode node = rootNode.SelectSingleNode(xpath);
			//return node;
		}


		// Suspected obsolete
		public LookupInfo.Key[] FindKeysForTable(LookupInfo.Key partialKey)
		{
		    // partial key needs a driver and connection
		    if (string.IsNullOrEmpty(partialKey.Driver) ||
		        string.IsNullOrEmpty(partialKey.Connection) ||
		        string.IsNullOrEmpty(partialKey.Table) )
		        throw new Exception(string.Format("LookupInfo2.FindKeysForTable called with missing key info: {0}", partialKey.ToString()));

		    // driver
		    string xpath = String.Format("/{0}/{1}[@{2}='{3}']/{4}[@{5}='{6}']/{7}[@{8}='{9}']", LUELM_DOC, LookupInfo.LUELM_DRIVER, LookupInfo.LUATT_NAME, partialKey.Driver,
		                                                                                    LookupInfo.LUELM_CONNECTION, LookupInfo.LUATT_NAME, partialKey.Connection,
		                                                                                    LookupInfo.LUELM_LOOKUP, LookupInfo.LUATT_TABLE, partialKey.Table);

		    return FindKeyList(partialKey, xpath);
		}

		private Key[] FindKeyList(LookupInfo.Key partialKey, string xpath)
		{
			XmlNodeList allLookups = this.SelectNodes(xpath);
			List<LookupInfo.Key> matchingKeys = new List<LookupInfo.Key>();
			foreach (XmlNode node in allLookups)
			{
				LookupInfo.Key k = new Key(partialKey.Driver, partialKey.Connection,
										this.AttributeValue(node, LookupInfo.LUATT_TABLE, ""),
										this.AttributeValue(node, LookupInfo.LUATT_FIELD, ""));
				matchingKeys.Add(k);
			}
			return matchingKeys.ToArray();
		}




		/////////////////////////////////////////////////////////////////////
		// Helpers
		private void RemoveRecord(XmlNode node)
		{
		    XmlNode parent = node.ParentNode;
		    if (parent != null)
		        parent.RemoveChild(node);
		}
		
		private void AddAttribute(XmlNode node, String attName, String attVal)
		{
		    XmlAttribute att = node.Attributes[attName];
		    if (att == null)
		    {
		        att = this.CreateAttribute(attName);
		        node.Attributes.Append(att);
		    }
		    att.Value = attVal;
		}
	
		private String AttributeValue(XmlNode node, String attName, String defaultVal)
		{
		    XmlAttribute att = node.Attributes[attName];
		    if (att == null) return defaultVal;
		    return att.Value;
		}
	}
}
